package main;

import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import clasificador.ValidadorCruzado;
import clasificador.vecinosproximos.ClasificadorVecinosProximos;
import datos.Atributo;
import datos.AtributoFinito;
import datos.CSVBuilder;
import datos.Dato;
import datos.Muestra;
import datos.MuestraClasificada;

public class MainKNN {
    private static PrintStream out = System.out;

    private static String parseFilename(String [] args) {
        boolean parsingParameter = false;
        for (String s : args) {
            if (s.startsWith("-")) {
                parsingParameter = true;
            } else if (! parsingParameter) {
                return s;
            } else {
                parsingParameter = false;
            }
        }
        return null;
    }
    private static String parseParameter(String [] args, String flag) {
        boolean foundFlag = false;
        for (String s : args) {
            if (s.equals(flag)) {
                foundFlag = true;
            } else if (foundFlag) {
                return s;
            }
        }
        return null;
    }
    public static void main(String [] args) {
        String filename = null;
        String outputFile = null;
        Integer k = null;
        try {
            filename = parseFilename(args);
            String strK = parseParameter(args, "-k");
            outputFile = parseParameter(args, "-log");
            if (strK == null) {
                k = 5;
            } else {
                k = Integer.parseInt(parseParameter(args, "-k"));
            }
        } catch (NumberFormatException ex) {
            System.out.println("Parametro numerico invalido");
            MainKNN.printUsage();
            MainKNN.exit(1);
        }
        if (filename == null) {
            System.out.println("Debe especificar un nombre de fichero");
            MainKNN.printUsage();
            MainKNN.exit(1);
        }
        if (outputFile != null) {
            System.out.println("Redirigiendo salida a " + outputFile);
            try {
                MainKNN.out = new PrintStream(new File(outputFile));
            } catch (Exception ex) {
                System.out.println("No se ha podido abrir el fichero " + outputFile);
                ex.printStackTrace(System.out);
            }
        }

        Map<String, Object> parametrosClasificacion = new HashMap<String, Object>();
        parametrosClasificacion.put("k", k);

        BufferedReader file;
        try {
            file = new BufferedReader(new FileReader(filename));
            MuestraClasificada<Object> muestra;

            List<Atributo<Object>> header = MainKNN.parseCSVHeader(file);
            out.println("Validando sobre la muestra " + filename);
            out.println("Atributos: ");
            for (Atributo<Object> atributo : header) {
                out.println("\t" + atributo.getNombre() + " : " + atributo.getTipo());
            }

            /* Esto es feo */
            Atributo<Object> clase = header.get(header.size() - 1);

            List<Atributo<?>> header2 = new Vector<Atributo<?>>(header);

            CSVBuilder<Dato, Muestra> builder =
                    new CSVBuilder<Dato, Muestra>(
                            new Dato.Builder(header),
                            new Muestra.Builder(header2)
                    );

            String line = file.readLine();
            while (line != null) {
                builder.add(line);
                line = file.readLine();
            }

            muestra = new MuestraClasificada<Object>(builder.finish(), clase);
            out.println("Leidas " + muestra.getDatos().size() + " entradas");

            Class<ClasificadorVecinosProximos> clasificador =
                    (ClasificadorVecinosProximos.class);
            ValidadorCruzado v = new ValidadorCruzado(clasificador, 10, 1);
            ValidadorCruzado.ResultadosValidacionCruzada resultados = v.validar(muestra,parametrosClasificacion);
            printResults(filename, resultados);
            file.close();

        } catch (Exception ex) {
            out.println( "Error al ejecutar la validacion!" );
            ex.printStackTrace(out);
        }
    }

    private static List<Atributo<Object>> parseCSVHeader(BufferedReader file) throws IOException {
        List<Atributo<Object>> header = new ArrayList<Atributo<Object>>();
        String line = file.readLine();
        while (!line.equals("")) {
            header.add(MainKNN.parseAttribute(line));
            line = file.readLine();
        }
        return header;
    }

    private static AtributoFinito<Object> parseFinite(String name, String type, String values) {
        if (values == null) {
            throw new RuntimeException("Malformed CSV header: no range for finite attribute " + name);
        }

        Set<String> valueSet = new HashSet<String>();
        for (String str : values.split("\\|")) {
            valueSet.add(str.trim());
        }

        if(type.equals("string"))
        {
            return (AtributoFinito)new AtributoFinito<String>(String.class, name, valueSet) {
                public String readDato(String s) { return s.trim(); }
            };
        } else if(type.equals("int")) {
            Set<Integer> intSet = new HashSet<Integer>();
            for (String s : valueSet) {
                intSet.add(Integer.parseInt(s));
            }
            return (AtributoFinito)new AtributoFinito<Integer>(Integer.class, name, intSet) {
                public Integer readDato(String s) { return Integer.parseInt(s); }
            };
        } else {
            throw new RuntimeException("Error parsing CSV header: undefined type " + type);
        }
    }

    private static Atributo<Object> parseInfiniteDiscrete(String name, String type) {
        if(type.equals("string"))
        {
            return (Atributo)new Atributo<String>(String.class, name) {
                public String readDato(String s) { return s.trim(); }
            };
        } else if(type.equals("int")) {
            return (Atributo)new Atributo<Integer>(Integer.class, name) {
                public Integer readDato(String s) { return Integer.parseInt(s); }
            };
        } else {
            throw new RuntimeException("Error parsing CSV header: undefined type " + type);
        }
    }
    private static Atributo<Object> parseAttribute(String s) {
        String [] tokens = s.split(":");
        if (tokens.length != 2 && tokens.length != 3) {
            throw new RuntimeException("Malformed or empty CSV header");
        }

        String name = tokens[0].trim();
        String type = tokens[1].trim();

        if(type.equals("string") || type.equals("int"))
        {
            if (tokens.length == 3) {
                return parseFinite(name, type, tokens[2]);
            } else {
                return parseInfiniteDiscrete(name,type);
            }
        } else if(type.equals("double")) {
            return (Atributo)new Atributo<Double> (Double.class, name) {
                public Double readDato(String s) { return Double.parseDouble(s); }
            };
        } else {
            throw new RuntimeException("Cabecera del fichero CSV inválida: tipo desconocido " + type);
        }
    }

    private static String executableName() {
        return new java.io.File(MainKNN.class.getProtectionDomain()
                .getCodeSource()
                .getLocation()
                .getPath())
                .getName();
    }
    private static void printResults(String filename,
                                     ValidadorCruzado.ResultadosValidacionCruzada resultados) {

        out.println(
                "Resultados para la muestra " + filename
        );
        int particion = 1;
        Double suma = 0.;
        for (Double error : resultados.getErrores()) {
            suma += error;
            out.println("\tParticion de test " + particion);
            out.println("\t\tProporcion de errores: " + error);
            out.println("\t\tProporcion de aciertos: " + (1.0 - error));
            particion++;
        }
        out.println("Error medio: " + (suma / resultados.getErrores().size()));
    }
    private static void printUsage() {
        out.println("Uso:");
        out.println("\tjava -jar "
                + MainKNN.executableName() + " <muestra> [-k <numero_vecinos>]"
        );
    }
    private static void exit(int k) {
        System.exit(k);
    }
}
